Skip to main content

copp\copp\copp2\opt2/
copp2_socp.rs

1//! 2nd-order Convex-Objective Path Parameterization (COPP2) based on second-order cone programming (SOCP).
2//!
3//! # Method identity
4//! This module implements the **optimization backend** for COPP2 by transforming path-parameterization
5//! constraints/objectives into a Clarabel-compatible conic form and solving it with SOCP.
6//!
7//! # Discrete variables (local notation)
8//! On a path grid `s[0..=n]`:
9//! - `a[k]` denotes $\dot{s}_k^2$ (state variable, expected nonnegative in feasible solutions);
10//! - decision vector is organized as `x = [a[0..=n], x_others]`, where `x_others` are auxiliary variables introduced by objective terms (e.g. reciprocal/soc slack variables);
11//!
12//! # High-level pipeline
13//! 1. Validate interval and boundary consistency.
14//! 2. Estimate capacities and assemble standard TOPP2 constraints.
15//! 3. Add COPP2 objective-induced variables/cones ([`Time`](crate::prelude::CoppObjective::Time), [`ThermalEnergy`](crate::prelude::CoppObjective::ThermalEnergy), [`TotalVariationTorque`](crate::prelude::CoppObjective::TotalVariationTorque), [`Linear`](crate::prelude::CoppObjective::Linear)).
16//! 4. Build sparse matrices `A`, `P`, vector `q`, and solve by Clarabel.
17//! 5. Apply status acceptance policy ([`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow)) and extract `a` when allowed.
18//!
19//! # API layering
20//! - [`copp2_socp`](crate::solver::copp2_socp::copp2_socp): strict/normal API, returns only accepted `a`.
21//! - [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert): expert API, always returns full Clarabel solution for diagnosis.
22//! - [`copp2_socp_expert_with_info`](crate::solver::copp2_socp::copp2_socp_expert_with_info): expert API plus Clarabel linear-solver
23//!   metadata for solver-side diagnostics.
24
25use crate::copp::clarabel_backend::{ConstraintsClarabel, ObjConsClarabel};
26use crate::copp::copp2::formulation::Copp2Problem;
27use crate::copp::copp2::opt2::ClarabelExpertInfor2nd;
28use crate::copp::copp2::opt2::clarabel_constraints::{
29    clarabel_standard_capacity_topp2, clarabel_standard_constraint_topp2,
30};
31use crate::copp::{ClarabelOptions, CoppObjective, clarabel_to_copp2_solution};
32use crate::diag::{
33    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
34    format_duration_human,
35};
36use crate::robot::robot_core::{Robot, RobotBasic, RobotTorque};
37use clarabel::algebra::CscMatrix;
38use clarabel::solver::SupportedConeT::{NonnegativeConeT, SecondOrderConeT};
39use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
40use core::f64;
41use itertools::{Itertools, izip};
42use nalgebra::{DMatrix, DVectorView};
43
44#[cfg(any(feature = "c", feature = "python", test))]
45use crate::copp::copp2::stable::basic::a_to_b_topp2;
46
47/// Strict COPP2-SOCP API for production use.
48///
49/// # Purpose
50/// Use this entry when caller only needs a valid trajectory profile `a` and treats
51/// non-accepted solver statuses as hard failures.
52///
53/// # Contract
54/// - Internally calls [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert).
55/// - Returns `Ok(a)` **iff** `options.is_allow(solution.status)` is `true`.
56/// - Returns `Err(CoppError::ClarabelSolverStatus(...))` when status is not accepted.
57///
58/// # Returns
59/// Returns accepted profile `a` for production usage.
60///
61/// # Errors
62/// Returns [`CoppError`](crate::diag::CoppError) for model/solver failures and when solver status is not accepted.
63///
64/// # Notes
65/// For workflows requiring low-level diagnostics (`status`, iterate behavior, residual-related fields in
66/// Clarabel solution), prefer [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert).
67pub fn copp2_socp<'a, M: RobotTorque>(
68    problem: &Copp2Problem<'a, M>,
69    options: &ClarabelOptions,
70) -> Result<Vec<f64>, CoppError> {
71    let (a_profile, solution) = copp2_socp_expert(problem, options)?;
72    a_profile.ok_or_else(|| CoppError::ClarabelSolverStatus("copp2_socp".into(), solution.status))
73}
74
75/// Expert COPP2-SOCP API with full Clarabel solution exposure.
76///
77/// # Purpose
78/// This API is intended for advanced users who need both:
79/// - extracted high-level profile `Option<Vec<f64>>`, and
80/// - raw solver result [`DefaultSolution<f64>`](clarabel::solver::DefaultSolution) for post-analysis.
81///
82/// # Return contract
83/// - `Ok((Some(a), solution))`: status accepted by `options.is_allow(solution.status)`.
84/// - `Ok((None, solution))`: solve finished but status not accepted by policy.
85/// - `Err(...)`: true runtime failures only (input validation / model build / solver construction).
86///
87/// # Returns
88/// Returns tuple `(Option<Vec<f64>>, DefaultSolution<f64>)` for diagnostic workflows.
89///
90/// # Errors
91/// Returns [`CoppError`](crate::diag::CoppError) only for real failures (input, model build, or solver runtime).
92///
93/// # Contract
94/// - caller must handle `None` profile when status is not accepted;
95/// - acceptance policy is fully controlled by `options.is_allow`.
96///   See [`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow)
97///   for a status-handling example.
98///
99/// # Verbosity behavior
100/// Logging is layered by `options.verbosity()`:
101/// - [`Silent`](crate::diag::Verbosity::Silent): no algorithm logs;
102/// - [`Summary`](crate::diag::Verbosity::Summary): lifecycle milestones and elapsed time;
103/// - [`Debug`](crate::diag::Verbosity::Debug): assembly-level counters and stage summaries;
104/// - [`Trace`](crate::diag::Verbosity::Trace): fine-grained stage deltas and solver snapshot diagnostics.
105pub fn copp2_socp_expert<'a, M: RobotTorque>(
106    problem: &Copp2Problem<'a, M>,
107    options: &ClarabelOptions,
108) -> Result<(Option<Vec<f64>>, DefaultSolution<f64>), CoppError> {
109    let result = copp2_socp_expert_with_info(problem, options)?;
110    Ok((result.result, result.solution))
111}
112
113/// Expert COPP2-SOCP API with Clarabel solution and linear-solver diagnostics.
114///
115/// Use this variant when callers need more than
116/// [`DefaultSolution`](clarabel::solver::DefaultSolution), because Clarabel stores linear solver metadata on the
117/// solver `info` object rather than inside the returned solution.
118///
119/// Status acceptance follows
120/// [`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow);
121/// see that method for the shared status-handling pattern.
122pub fn copp2_socp_expert_with_info<'a, M: RobotTorque>(
123    problem: &Copp2Problem<'a, M>,
124    options: &ClarabelOptions,
125) -> Result<ClarabelExpertInfor2nd, CoppError> {
126    match options.verbosity() {
127        Verbosity::Silent => copp2_socp_core(problem, (options, SilentVerboser)),
128        Verbosity::Summary => copp2_socp_core(problem, (options, SummaryVerboser::new())),
129        Verbosity::Debug => copp2_socp_core(problem, (options, DebugVerboser::new())),
130        Verbosity::Trace => copp2_socp_core(problem, (options, TraceVerboser::new())),
131    }
132}
133
134/// Core implementation for COPP2-SOCP expert flow.
135///
136/// # Internal contract
137/// `options_verboser` packs:
138/// - `options`: acceptance policy and Clarabel numerical settings;
139/// - `verboser`: concrete logger implementation chosen by external verbosity dispatch.
140///
141/// # Invariants
142/// - decision-variable layout always starts with contiguous `a[0..=n]`;
143/// - `q_object.len()` is treated as final `n_var` before solver build;
144/// - extracted `a` is produced only through [`clarabel_to_copp2_solution`](crate::solver::copp2_socp::clarabel_to_copp2_solution) when status is accepted.
145fn copp2_socp_core<'a, M: RobotTorque>(
146    problem: &Copp2Problem<'a, M>,
147    options_verboser: (&ClarabelOptions, impl Verboser),
148) -> Result<ClarabelExpertInfor2nd, CoppError> {
149    let (options, mut verboser) = options_verboser;
150    let (idx_s_start, idx_s_final) = problem.idx_s_interval;
151    if verboser.is_enabled(Verbosity::Summary) {
152        verboser.record_start_time();
153        crate::verbosity_log!(
154            crate::diag::Verbosity::Summary,
155            "\ncopp2_socp started: {} <= idx_s <= {}, objectives = {}, s_len = {}.",
156            idx_s_start,
157            idx_s_final,
158            problem.objectives.len(),
159            problem.s_len()
160        );
161    }
162    if verboser.is_enabled(Verbosity::Trace) {
163        let settings = options.clarabel_settings();
164        crate::verbosity_log!(
165            crate::diag::Verbosity::Summary,
166            "copp2_socp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
167            options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
168            options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
169            options.is_allow(clarabel::solver::SolverStatus::MaxTime),
170            options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
171            options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
172            settings.tol_gap_rel,
173            settings.tol_feas,
174            settings.max_iter,
175            settings.verbose
176        );
177    }
178    // Check input validity
179    let n = idx_s_final - idx_s_start;
180    // Let x = [a[0], a[1], ..., a[n], x_others] \in R^{n+1+n_others}.
181    // Step 1. Deal with constraints
182    // Step 1.1 Compute the number of constraints
183    let (cap_val_std, cap_b_std, cap_cone_std) =
184        clarabel_standard_capacity_topp2(&problem.robot.constraints, problem.idx_s_interval);
185    let (cap_val_obj, cap_b_obj, cap_cone_obj, n_vars) =
186        clarabel_objective_capacity_copp2(n, problem.objectives, problem.robot);
187    if verboser.is_enabled(Verbosity::Debug) {
188        crate::verbosity_log!(
189            crate::diag::Verbosity::Summary,
190            "copp2_socp: capacity estimate std(val={cap_val_std}, b={cap_b_std}, cone={cap_cone_std}), obj(val={cap_val_obj}, b={cap_b_obj}, cone={cap_cone_obj}), n_vars={n_vars}."
191        );
192    }
193    // s=b-A*x \in cone, where A[row[i],col[i]]=val[i], A \in R^{m*(n+1)}, b \in R^m, s \in R^m
194    // -s=-b+A*x
195    let mut row = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
196    let mut col = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
197    let mut val = Vec::<f64>::with_capacity(cap_val_std + cap_val_obj);
198    let mut b = Vec::<f64>::with_capacity(cap_b_std + cap_b_obj);
199    let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(cap_cone_std + cap_cone_obj);
200    if verboser.is_enabled(Verbosity::Trace) {
201        crate::verbosity_log!(
202            crate::diag::Verbosity::Summary,
203            "copp2_socp: allocated capacities row/col/val/b/cones <= {}/{}/{}/{}/{}",
204            cap_val_std + cap_val_obj,
205            cap_val_std + cap_val_obj,
206            cap_val_std + cap_val_obj,
207            cap_b_std + cap_b_obj,
208            cap_cone_std + cap_cone_obj
209        );
210    }
211    // Step 1.2 set constraints
212    let row_before_std = row.len();
213    let col_before_std = col.len();
214    let val_before_std = val.len();
215    let b_before_std = b.len();
216    let cones_before_std = cones.len();
217    clarabel_standard_constraint_topp2(
218        &problem.as_topp2_problem(),
219        (&mut row, &mut col, &mut val, &mut b, &mut cones),
220        &verboser,
221    );
222    if verboser.is_enabled(Verbosity::Trace) {
223        crate::verbosity_log!(
224            crate::diag::Verbosity::Summary,
225            "copp2_socp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
226            row.len() - row_before_std,
227            col.len() - col_before_std,
228            val.len() - val_before_std,
229            b.len() - b_before_std,
230            cones.len() - cones_before_std
231        );
232    }
233    // Step 2. set objective
234    // Step 2.1. determine whether eta=1/sqrt(a) is needed.
235    let row_before_sqrt = row.len();
236    let col_before_sqrt = col.len();
237    let val_before_sqrt = val.len();
238    let b_before_sqrt = b.len();
239    let cones_before_sqrt = cones.len();
240    let n_var_old = clarabel_sqrt_a_copp2(
241        n,
242        problem.objectives,
243        (&mut row, &mut col, &mut val, &mut b, &mut cones),
244    );
245    if verboser.is_enabled(Verbosity::Trace) {
246        crate::verbosity_log!(
247            crate::diag::Verbosity::Summary,
248            "copp2_socp: sqrt-a stage delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}, n_var_old={}",
249            row.len() - row_before_sqrt,
250            col.len() - col_before_sqrt,
251            val.len() - val_before_sqrt,
252            b.len() - b_before_sqrt,
253            cones.len() - cones_before_sqrt,
254            n_var_old
255        );
256    }
257    let mut q_object = Vec::<f64>::with_capacity(n_vars);
258    q_object.resize(n_var_old, 0.0);
259    // Step 2.2. add constraints and objective for each term in the objective.
260    let row_before_obj = row.len();
261    let col_before_obj = col.len();
262    let val_before_obj = val.len();
263    let b_before_obj = b.len();
264    let cones_before_obj = cones.len();
265    let q_before_obj = q_object.len();
266    clarable_objective_copp2(
267        problem,
268        (
269            &mut row,
270            &mut col,
271            &mut val,
272            &mut b,
273            &mut cones,
274            &mut q_object,
275        ),
276    )?;
277    if verboser.is_enabled(Verbosity::Trace) {
278        let (q_min, q_max) = q_object
279            .iter()
280            .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
281                (mn.min(v), mx.max(v))
282            });
283        crate::verbosity_log!(
284            crate::diag::Verbosity::Summary,
285            "copp2_socp: objective stage delta row/col/val/b/cones/q = +{}/+{}/+{}/+{}/+{}/+{}, q_range=[{}, {}]",
286            row.len() - row_before_obj,
287            col.len() - col_before_obj,
288            val.len() - val_before_obj,
289            b.len() - b_before_obj,
290            cones.len() - cones_before_obj,
291            q_object.len() - q_before_obj,
292            q_min,
293            q_max
294        );
295    }
296    if verboser.is_enabled(Verbosity::Debug) {
297        crate::verbosity_log!(
298            crate::diag::Verbosity::Summary,
299            "copp2_socp: after objective assembly row={}, col={}, val={}, b={}, cones={}, q={}",
300            row.len(),
301            col.len(),
302            val.len(),
303            b.len(),
304            cones.len(),
305            q_object.len()
306        );
307    }
308    if verboser.is_enabled(Verbosity::Summary) {
309        crate::verbosity_log!(
310            crate::diag::Verbosity::Summary,
311            "copp2_socp: ready to solve with row/col/val/b/cones = {}/{}/{}/{}/{} and n_var = {}.",
312            row.len(),
313            col.len(),
314            val.len(),
315            b.len(),
316            cones.len(),
317            q_object.len()
318        );
319    }
320    // Step 2.3 build the constraints
321    let n_var = q_object.len();
322    let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
323    let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
324    if verboser.is_enabled(Verbosity::Trace) {
325        crate::verbosity_log!(
326            crate::diag::Verbosity::Summary,
327            "copp2_socp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}",
328            b.len(),
329            n_var,
330            a_csc.nnz(),
331            p_object.nnz()
332        );
333    }
334    // Step 3. solve the SOCP problem
335    let settings = options.clarabel_settings().clone();
336    let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
337        .map_err(|e| CoppError::ClarabelSolverError("copp2_socp".into(), e))?;
338    solver.solve();
339    let linsolver = solver.info.linsolver.clone();
340    let solution = solver.solution;
341    if verboser.is_enabled(Verbosity::Summary) {
342        crate::verbosity_log!(
343            crate::diag::Verbosity::Summary,
344            "copp2_socp: solve done, status = {:?}, elapsed = {}.",
345            solution.status,
346            format_duration_human(verboser.elapsed())
347        );
348    }
349    if verboser.is_enabled(Verbosity::Trace) {
350        let show = solution.x.len().min(3);
351        crate::verbosity_log!(
352            crate::diag::Verbosity::Summary,
353            "copp2_socp: solution x_len={}, head={:?}",
354            solution.x.len(),
355            &solution.x[0..show]
356        );
357    }
358    let a_profile = if options.is_allow(solution.status) {
359        Some(clarabel_to_copp2_solution(problem.s_len(), &solution))
360    } else {
361        None
362    };
363    if verboser.is_enabled(Verbosity::Trace) {
364        crate::verbosity_log!(
365            crate::diag::Verbosity::Summary,
366            "copp2_socp: allow(status)={}, extracted_profile={}",
367            options.is_allow(solution.status),
368            if a_profile.is_some() {
369                "Some(a)"
370            } else {
371                "None"
372            }
373        );
374    }
375    Ok(ClarabelExpertInfor2nd {
376        result: a_profile,
377        solution,
378        linsolver,
379    })
380}
381
382/// Determine the number of clarabel's capacity for the objective in COPP2.
383fn clarabel_objective_capacity_copp2<M: RobotBasic>(
384    n: usize,
385    objective: &[CoppObjective],
386    robot: &Robot<M>,
387) -> (usize, usize, usize, usize) {
388    let flag_need_eta = objective.iter().any(|obj| {
389        matches!(
390            obj,
391            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
392        )
393    });
394    // Step 1. sqrt(a[k]) >= eta[k] >= 0
395    // num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2
396    let (mut capacity_val, mut capacity_b, mut capacity_cones, mut n_vars) = if flag_need_eta {
397        (4 * (n + 1), 4 * (n + 1), n + 2, 2 * (n + 1))
398    } else {
399        (0, 0, 0, n + 1)
400    };
401    // Step 2. objective function
402    let dim = robot.dim();
403    for obj in objective {
404        match obj {
405            CoppObjective::Time(_) => {
406                // num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
407                capacity_val += 6 * n;
408                capacity_b += 3 * n;
409                capacity_cones += n;
410                n_vars += n;
411            }
412            CoppObjective::ThermalEnergy(_, _) => {
413                // num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
414                capacity_val += (6 + 2 * dim) * n;
415                capacity_b += (dim + 2) * n;
416                capacity_cones += n;
417                n_vars += n;
418            }
419            CoppObjective::TotalVariationTorque(_, _) => {
420                // num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
421                capacity_val += 8 * dim * n;
422                capacity_b += 2 * dim * n;
423                capacity_cones += 1;
424                n_vars += dim * n;
425            }
426            _ => {}
427        }
428    }
429    (capacity_val, capacity_b, capacity_cones, n_vars)
430}
431
432/// Add the constraints for sqrt(a) >= eta in COPP2 optimization.
433/// x = [a[0], a[1], ..., a[n], eta[0], eta[1], ..., eta[n], ...] \in R^{2*(n+1)+...}.
434/// sqrt(a[k]) >= eta[k] >= 0
435/// num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2
436/// Return the len of the new x: n+1 or 2*(n+1)
437fn clarabel_sqrt_a_copp2(
438    n: usize,
439    objective: &[CoppObjective],
440    constraints: ConstraintsClarabel,
441) -> usize {
442    let (row, col, val, b, cones) = constraints;
443    for obj in objective {
444        match obj {
445            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _) => {
446                // eta >= 0
447                // A*x-b = -s = -1*eta[k] <= 0
448                row.extend(b.len()..b.len() + n + 1);
449                col.extend((n + 1)..(2 * (n + 1)));
450                val.resize(val.len() + n + 1, -1.0);
451                b.resize(b.len() + n + 1, 0.0);
452                cones.push(NonnegativeConeT(n + 1));
453                // sqrt(a) >= eta
454                // eta^2 <= a
455                // eta^2 + (a - 0.25)^2 <= (a + 0.25)^2
456                // -A*x+b = s = [a+0.25, a-0.25, eta] \in SOC
457                row.extend(b.len()..b.len() + 3 * (n + 1));
458                val.resize(val.len() + 3 * (n + 1), -1.0);
459                cones.resize(cones.len() + n + 1, SecondOrderConeT(3));
460                for k in 0..=n {
461                    col.extend([k, k, k + n + 1]);
462                    b.extend([0.25, -0.25, 0.0]);
463                }
464                return 2 * (n + 1);
465            }
466            _ => {}
467        }
468    }
469    n + 1
470}
471
472/// Add the constraints and objective for Time in COPP2 optimization.
473/// num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
474fn clarabel_objective_time_copp2(
475    s: &[f64],
476    weight: f64,
477    objective_constraints: ObjConsClarabel,
478) -> bool {
479    if weight < 0.0 {
480        return false;
481    }
482    let (row, col, val, b, cones, q_object) = objective_constraints;
483    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1])
484    // Let: 1 / (eta[k] + eta[k+1]) <= 4 * t[k]
485    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
486    let weight = 8.0 * weight;
487    let n_var_old = q_object.len();
488    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
489    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
490    // t[k] * (eta[k] + eta[k+1]) >= 1
491    // (eta[k] + eta[k+1] + t[k])^2 >= (eta[k] + eta[k+1] - t[k])^2 + 1
492    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], 1] \in SOC
493    let len = s.len();
494    for k in 0..(len - 1) {
495        // eta[k] + eta[k+1] + t[k]
496        row.resize(row.len() + 3, b.len());
497        col.extend([len + k, len + k + 1, n_var_old + k]);
498        val.extend([-1.0, -1.0, -1.0]);
499        b.push(0.0);
500        // eta[k] + eta[k+1] - t[k]
501        row.resize(row.len() + 3, b.len());
502        col.extend([len + k, len + k + 1, n_var_old + k]);
503        val.extend([-1.0, -1.0, 1.0]);
504        b.push(0.0);
505        // 1
506        b.push(1.0);
507    }
508    cones.resize(cones.len() + len - 1, SecondOrderConeT(3));
509    true
510}
511
512/// Add the constraints and objective for ThermalEnergy in COPP2 optimization.
513/// num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
514fn clarabel_objective_thermal_energy_copp2(
515    s: &[f64],
516    weight: f64,
517    normalize: &[f64],
518    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
519    objective_constraints: ObjConsClarabel,
520) -> bool {
521    if weight < 0.0 {
522        return false;
523    }
524    let (row, col, val, b, cones, q_object) = objective_constraints;
525    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
526    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1]) * (tau[i][k] * normalize[i]) ^ 2
527    // Let: \sum_i (tau[i][k] * normalize[i]) ^ 2 / (eta[k] + eta[k+1]) <= 4 * t[k]
528    let len = s.len();
529    let mut coeff_a_curr = coeffs_torque.0.clone();
530    let mut coeff_a_next = coeffs_torque.1.clone();
531    let mut coeff_g = coeffs_torque.2.clone();
532    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
533    let weight = 8.0 * weight;
534    let n_var_old = q_object.len();
535    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
536    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
537    // \sum_i (tau[i][k] * normalize[i]) ^ 2 <= 4 * t[k] * (eta[k] + eta[k+1])
538    let dim = coeff_a_curr.nrows();
539    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
540    if normalize.len() != dim {
541        return false;
542    }
543    let normalize = DVectorView::from_slice(normalize, dim);
544    for mut col in coeff_a_curr.column_iter_mut() {
545        col.component_mul_assign(&normalize);
546    }
547    for mut col in coeff_a_next.column_iter_mut() {
548        col.component_mul_assign(&normalize);
549    }
550    for mut col in coeff_g.column_iter_mut() {
551        col.component_mul_assign(&normalize);
552    }
553    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
554
555    // (eta[k] + eta[k+1] - t[k])^2 + \sum_i (tau[i][k] * normalize[i]) ^ 2 <= (eta[k] + eta[k+1] + t[k])^2
556    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], tau[0][k] * normalize[0], tau[1][k] * normalize[1], ...] \in SOC
557    for (k, (col_a_curr, col_a_next, col_g)) in izip!(
558        coeff_a_curr.column_iter(),
559        coeff_a_next.column_iter(),
560        coeff_g.column_iter()
561    )
562    .enumerate()
563    {
564        // eta[k] + eta[k+1] + t[k]
565        row.resize(row.len() + 3, b.len());
566        col.extend([len + k, len + k + 1, n_var_old + k]);
567        val.extend([-1.0, -1.0, -1.0]);
568        b.push(0.0);
569        // eta[k] + eta[k+1] - t[k]
570        row.resize(row.len() + 3, b.len());
571        col.extend([len + k, len + k + 1, n_var_old + k]);
572        val.extend([-1.0, -1.0, 1.0]);
573        b.push(0.0);
574        // tau[i][k] * normalize[i] = col_a_curr[i] * x[k] + col_a_next[i] * x[k+1] + col_g[i]
575        for (&v_a_curr, &v_a_next, &v_g) in
576            izip!(col_a_curr.iter(), col_a_next.iter(), col_g.iter())
577        {
578            row.resize(row.len() + 2, b.len());
579            col.extend([k, k + 1]);
580            val.extend([v_a_curr, v_a_next]);
581            b.push(v_g);
582        }
583    }
584    cones.resize(cones.len() + len - 1, SecondOrderConeT(dim + 2));
585    true
586}
587
588/// Add the constraints and objective for TotalVariationTorque in COPP2 optimization.
589/// num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
590fn clarabel_objective_tv_torque_copp2(
591    weight: f64,
592    normalize: &[f64],
593    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
594    objective_constraints: ObjConsClarabel,
595) -> bool {
596    if weight < 0.0 {
597        return false;
598    }
599    let (row, col, val, b, cones, q_object) = objective_constraints;
600    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
601    // Let: |tau[i][k+1]-tau[i][k]| * normalize[i] <= t[i][k]
602    let mut coeff_a_curr = coeffs_torque.0.clone();
603    let mut coeff_a_next = coeffs_torque.1.clone();
604    let mut coeff_g = coeffs_torque.2.clone();
605    let dim = coeff_a_curr.nrows();
606    if normalize.len() != dim {
607        return false;
608    }
609    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
610    let normalize = DVectorView::from_slice(normalize, dim);
611    for mut col in coeff_a_curr.column_iter_mut() {
612        col.component_mul_assign(&normalize);
613    }
614    for mut col in coeff_a_next.column_iter_mut() {
615        col.component_mul_assign(&normalize);
616    }
617    for mut col in coeff_g.column_iter_mut() {
618        col.component_mul_assign(&normalize);
619    }
620    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
621    // (tau[i][k+1]-tau[i][k]) * normalize[i] = (coeff_a_curr[i][k+1] * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + coeff_g[i][k+1]) - (coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k])
622    // = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k])
623
624    let n_b_old = b.len();
625    // A*x-b = -s = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
626    // A*x-b = -s = coeff_a_curr[i][k] * a[k] - (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] - coeff_a_next[i][k+1] * a[k+2] - (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
627    let mut buffer0 = vec![0.0; dim];
628    let mut buffer1 = vec![0.0; dim];
629    for (k, ((col_a_curr, col_b_curr, col_g_curr), (col_a_next, col_b_next, col_g_next))) in izip!(
630        coeff_a_curr.column_iter(),
631        coeff_a_next.column_iter(),
632        coeff_g.column_iter()
633    )
634    .tuple_windows()
635    .enumerate()
636    {
637        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + (col_a_next[i] - col_b_curr[i]) * a[k+1] + col_b_next[i] * a[k+2] + (col_g_next[i] - col_g_curr[i])
638        buffer0.clear();
639        buffer1.clear();
640        buffer0.extend(
641            col_a_next
642                .iter()
643                .zip(col_b_curr.iter())
644                .map(|(&v_a_next, &v_b_curr)| v_b_curr - v_a_next),
645        );
646        buffer1.extend(
647            col_g_curr
648                .iter()
649                .zip(col_g_next.iter())
650                .map(|(&v_g_curr, &v_g_next)| v_g_curr - v_g_next),
651        );
652        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i]
653
654        let n_var_old = q_object.len();
655        for (i, (&v0, &v1, &v_a_curr, &v_b_next)) in izip!(
656            buffer0.iter(),
657            buffer1.iter(),
658            col_a_curr.iter(),
659            col_b_next.iter()
660        )
661        .enumerate()
662        {
663            // -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] <= t[i][k]
664            // A*x-b = -s = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] - t[i][k] <= 0
665            row.resize(row.len() + 4, b.len());
666            col.extend([k, k + 1, k + 2, n_var_old + i]);
667            val.extend([-v_a_curr, v0, v_b_next, -1.0]);
668            b.push(v1);
669
670            // -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) <= t[i][k]
671            // A*x-b = -s = -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) - t[i][k] <= 0
672            row.resize(row.len() + 4, b.len());
673            col.extend([k, k + 1, k + 2, n_var_old + i]);
674            val.extend([v_a_curr, -v0, -v_b_next, -1.0]);
675            b.push(-v1);
676        }
677
678        // objective: minimize weight * \sum t[i][k]
679        q_object.resize(q_object.len() + dim, weight);
680    }
681    cones.push(NonnegativeConeT(b.len() - n_b_old));
682    true
683}
684
685/// Add the constraints and objective for Linear in COPP2 optimization.
686fn clarabel_objective_linear_copp2(
687    s: &[f64],
688    weight: f64,
689    alpha: &[f64],
690    beta: &[f64],
691    q_object: &mut [f64],
692) -> bool {
693    if alpha.len() != s.len() || beta.len() != s.len() - 1 {
694        return false;
695    }
696    // objective: minimize weight * \sum (alpha[k]*a[k] + beta[k]*b[k])
697    // weight * \sum alpha[k]*a[k] + 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
698    for (va, q) in alpha.iter().zip(q_object.iter_mut()) {
699        // weight * \sum alpha[k]*a[k]
700        *q += weight * va;
701    }
702    for (s_pair, vb, q_curr) in izip!(s.windows(2), beta.iter(), q_object.iter_mut()) {
703        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
704        *q_curr -= 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
705    }
706    for (s_pair, vb, q_next) in izip!(s.windows(2), beta.iter(), q_object.iter_mut().skip(1)) {
707        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
708        *q_next += 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
709    }
710    true
711}
712
713fn clarable_objective_copp2<M: RobotTorque>(
714    problem: &Copp2Problem<M>,
715    objective_constraints: ObjConsClarabel,
716) -> Result<(), CoppError> {
717    let (row, col, val, b, cones, q_object) = objective_constraints;
718    let s = problem
719        .robot
720        .constraints
721        .s_vec(problem.idx_s_interval.0, problem.idx_s_interval.1 + 1)?;
722    let coeffs_torque = if problem.objectives.iter().any(|obj| {
723        matches!(
724            obj,
725            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
726        )
727    }) {
728        // shape: (dim, n) since there are n+1 a and n b.
729        problem.robot.torque2_coeff_a(
730            problem.idx_s_interval.0,
731            problem.idx_s_interval.1 - problem.idx_s_interval.0,
732        )?
733    } else {
734        (
735            DMatrix::<f64>::zeros(0, 0),
736            DMatrix::<f64>::zeros(0, 0),
737            DMatrix::<f64>::zeros(0, 0),
738        )
739    };
740    for obj in problem.objectives {
741        match obj {
742            CoppObjective::Time(weight) => {
743                if !clarabel_objective_time_copp2(&s, *weight, (row, col, val, b, cones, q_object))
744                {
745                    return Err(CoppError::InvalidInput(
746                        "copp2_socp".into(),
747                        "Invalid Time objective.".into(),
748                    ));
749                }
750            }
751            CoppObjective::ThermalEnergy(weight, normalize) => {
752                if !clarabel_objective_thermal_energy_copp2(
753                    &s,
754                    *weight,
755                    normalize,
756                    &coeffs_torque,
757                    (row, col, val, b, cones, q_object),
758                ) {
759                    return Err(CoppError::InvalidInput(
760                        "copp2_socp".into(),
761                        "Invalid ThermalEnergy objective.".into(),
762                    ));
763                }
764            }
765            CoppObjective::TotalVariationTorque(weight, normalize) => {
766                if !clarabel_objective_tv_torque_copp2(
767                    *weight,
768                    normalize,
769                    &coeffs_torque,
770                    (row, col, val, b, cones, q_object),
771                ) {
772                    return Err(CoppError::InvalidInput(
773                        "copp2_socp".into(),
774                        "Invalid TotalVariationTorque objective.".into(),
775                    ));
776                }
777            }
778            CoppObjective::Linear(weight, alpha, beta) => {
779                if !clarabel_objective_linear_copp2(&s, *weight, alpha, beta, q_object) {
780                    return Err(CoppError::InvalidInput(
781                        "copp2_socp".into(),
782                        "Invalid Linear objective.".into(),
783                    ));
784                }
785            }
786        }
787    }
788    Ok(())
789}
790
791/// Compute the objective value for COPP2 optimization.
792#[cfg(any(feature = "c", feature = "python", test))]
793pub(crate) fn objective_value_copp2_opt<M: RobotTorque>(
794    robot: &Robot<M>,
795    start_idx_s: usize,
796    objective: &[CoppObjective],
797    a_profile: &[f64],
798) -> (f64, Vec<f64>) {
799    let Ok(s) = robot
800        .constraints
801        .s_vec(start_idx_s, start_idx_s + a_profile.len())
802    else {
803        return (f64::INFINITY, vec![0.0; objective.len()]);
804    };
805    if a_profile.len() != s.len() {
806        return (f64::INFINITY, vec![0.0; objective.len()]);
807    }
808    let Ok(b_profile) = a_to_b_topp2(&s, a_profile) else {
809        return (f64::INFINITY, vec![0.0; objective.len()]);
810    };
811    let torque = if objective.iter().any(|obj| {
812        matches!(
813            obj,
814            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
815        )
816    }) {
817        let torque_result =
818            robot.get_torque_with_ab(&a_profile[0..a_profile.len() - 1], &b_profile, start_idx_s);
819        match torque_result {
820            Ok(torque) => torque,
821            _ => return (f64::INFINITY, vec![0.0; objective.len()]),
822        }
823    } else {
824        DMatrix::<f64>::zeros(0, 0)
825    };
826    let a_sqrt = if objective.iter().any(|obj| {
827        matches!(
828            obj,
829            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
830        )
831    }) {
832        a_profile.iter().map(|a| a.sqrt()).collect()
833    } else {
834        Vec::new()
835    };
836    let mut obj_val = Vec::with_capacity(objective.len());
837    let mut obj_val_total = 0.0;
838    for obj in objective {
839        match obj {
840            CoppObjective::Time(weight) => {
841                let obj_here = objective_value_time_copp2(&s, &a_sqrt);
842                obj_val.push(obj_here);
843                obj_val_total += weight * obj_here;
844            }
845            CoppObjective::ThermalEnergy(weight, normalize) => {
846                let obj_here =
847                    objective_value_thermal_energy_copp2(&s, &a_sqrt, &torque, normalize);
848                obj_val.push(obj_here);
849                obj_val_total += weight * obj_here;
850            }
851            CoppObjective::TotalVariationTorque(weight, normalize) => {
852                let obj_here = objective_value_tv_torque_copp2(&torque, normalize);
853                obj_val.push(obj_here);
854                obj_val_total += weight * obj_here;
855            }
856            CoppObjective::Linear(weight, alpha, beta) => {
857                let obj_here = objective_value_linear_copp2(&s, a_profile, alpha, beta);
858                obj_val.push(obj_here);
859                obj_val_total += weight * obj_here;
860            }
861        }
862    }
863    (obj_val_total, obj_val)
864}
865
866/// Compute the time value in COPP2 optimization.
867/// Input: s, a_sqrt = sqrt(a)
868#[cfg(any(feature = "c", feature = "python", test))]
869#[inline(always)]
870fn objective_value_time_copp2(s: &[f64], a_sqrt: &[f64]) -> f64 {
871    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1]))
872    let mut objective = 0.0;
873    for (s_pair, a_sqrt_pair) in s.windows(2).zip(a_sqrt.windows(2)) {
874        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]);
875    }
876    2.0 * objective
877}
878
879/// Compute the thermal energy value in COPP2 optimization.
880#[cfg(any(feature = "c", feature = "python", test))]
881#[inline(always)]
882fn objective_value_thermal_energy_copp2(
883    s: &[f64],
884    a_sqrt: &[f64],
885    torque: &DMatrix<f64>,
886    normalize: &[f64],
887) -> f64 {
888    // minimize: 2 * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
889    let mut objective = 0.0;
890    for (s_pair, a_sqrt_pair, torque_col) in
891        izip!(s.windows(2), a_sqrt.windows(2), torque.column_iter())
892    {
893        let mut sum = 0.0;
894        for (torque, normal) in torque_col.iter().zip(normalize.iter()) {
895            sum += (torque * normal).powi(2);
896        }
897        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]) * sum;
898    }
899    2.0 * objective
900}
901
902/// Compute the total variation of torque value in COPP2 optimization.
903#[cfg(any(feature = "c", feature = "python", test))]
904#[inline(always)]
905fn objective_value_tv_torque_copp2(torque: &DMatrix<f64>, normalize: &[f64]) -> f64 {
906    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
907    let mut objective = 0.0;
908    for (torque_col_curr, torque_col_next) in torque.column_iter().tuple_windows() {
909        for (torque_prev, torque_next, normal) in izip!(
910            torque_col_curr.iter(),
911            torque_col_next.iter(),
912            normalize.iter()
913        ) {
914            objective += (torque_next - torque_prev).abs() * normal;
915        }
916    }
917    objective
918}
919
920/// Compute the objective value for Linear in COPP2 optimization.
921#[cfg(any(feature = "c", feature = "python", test))]
922#[inline(always)]
923fn objective_value_linear_copp2(s: &[f64], a_profile: &[f64], alpha: &[f64], beta: &[f64]) -> f64 {
924    // objective: minimize \sum (alpha[k]*a[k] + beta[k]*b[k])
925    // \sum alpha[k]*a[k] + 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
926    let mut objective = 0.0;
927    for (a_curr, alpha_curr) in a_profile.iter().zip(alpha.iter()) {
928        // alpha[k]*a[k]
929        objective += a_curr * alpha_curr;
930    }
931    for (a_pair, s_pair, beta_curr) in izip!(a_profile.windows(2), s.windows(2), beta.iter()) {
932        // 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
933        objective += 0.5 * beta_curr * (a_pair[1] - a_pair[0]) / (s_pair[1] - s_pair[0]);
934    }
935    objective
936}
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941    use crate::copp::copp2::stable::basic::{
942        Copp2ProblemBuilder, Topp2ProblemBuilder, s_to_t_topp2,
943    };
944    use crate::copp::copp2::stable::reach_set2::{ReachSet2Options, ReachSet2OptionsBuilder};
945    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
946    use crate::copp::{ClarabelOptions, ClarabelOptionsBuilder};
947    use crate::path::{
948        Path, SplineConfig, add_symmetric_axial_limits_for_test, lissajous_path_for_test,
949    };
950    use crate::robot::demo::Plannar2LinkEnd;
951    use crate::robot::robot_core::Robot;
952    use core::panic;
953    use nalgebra::DMatrix;
954    use std::time::Instant;
955    use std::vec;
956
957    #[test]
958    fn test_copp2_socp_only_time() -> Result<(), CoppError> {
959        run_test_copp2_socp_only_time_repeated(1, false)
960    }
961
962    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
963    /// Average 100 experiments: tc_ra = 0.2005 ms, tc_lp = 29.7361 ms, tc_qp = 166.5122 ms, tf_ra = 4.766745, tf_lp = 4.766745, tf_qp = 4.766735
964    #[test]
965    #[ignore = "slow"]
966    fn test_copp2_socp_only_time_robust() -> Result<(), CoppError> {
967        run_test_copp2_socp_only_time_repeated(100, true)?;
968        Ok(())
969    }
970
971    #[test]
972    fn test_copp2_socp() -> Result<(), CoppError> {
973        let options_socp = ClarabelOptionsBuilder::new()
974            .allow_almost_solved(true)
975            .build()?;
976        run_test_copp2_socp_once(&options_socp)
977    }
978
979    #[test]
980    #[ignore = "bindings"]
981    fn test_copp2_socp_bindings_parity() -> Result<(), CoppError> {
982        let dim = 3;
983        let num_waypoints = 8;
984        let n: usize = 81;
985        let pi = std::f64::consts::PI;
986
987        let waypoints = DMatrix::<f64>::from_fn(dim, num_waypoints, |axis, j| {
988            let s = j as f64 / (num_waypoints - 1) as f64;
989            match axis {
990                0 => 0.20 * (2.0 * pi * s).sin(),
991                1 => 0.15 * (1.5 * pi * s).cos(),
992                2 => 0.10 * s * (1.0 - s),
993                _ => unreachable!("dimension is fixed to 3"),
994            }
995        });
996        let path = Path::from_waypoints(&waypoints, SplineConfig::default())?;
997        let s = DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64);
998
999        let mut robot = Robot::with_capacity(dim, n);
1000        robot
1001            .with_s(&s.as_view())?
1002            .with_q_from_path_2nd(&path, 0, n)?;
1003        add_symmetric_axial_limits_for_test(&mut robot, 10.0, 50.0, None)?;
1004        let torque_max = vec![1.0e6; dim];
1005        let torque_min = vec![-1.0e6; dim];
1006        robot.with_axial_torque((torque_max.as_slice(), n), (torque_min.as_slice(), n), 0)?;
1007
1008        let normalize = vec![1.0; dim];
1009        let options = ClarabelOptionsBuilder::new()
1010            .allow_almost_solved(true)
1011            .build()?;
1012
1013        let objectives_thermal = [
1014            CoppObjective::Time(1.0),
1015            CoppObjective::ThermalEnergy(1.0, &normalize),
1016        ];
1017        let problem_thermal =
1018            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &objectives_thermal)
1019                .build()?;
1020        let a_thermal = copp2_socp(&problem_thermal, &options)?;
1021        let (t_final_thermal, _) = s_to_t_topp2(s.as_slice(), &a_thermal, 0.0)?;
1022
1023        let objectives_tv = [
1024            CoppObjective::Time(1.0),
1025            CoppObjective::TotalVariationTorque(1.0, &normalize),
1026        ];
1027        let problem_tv =
1028            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &objectives_tv).build()?;
1029        let a_tv = copp2_socp(&problem_tv, &options)?;
1030
1031        crate::verbosity_log!(
1032            crate::diag::Verbosity::Summary,
1033            "COPP2-SOCP Rust bindings parity test: t_final_thermal={:.17}, thermal.len={}, tv.len={}",
1034            t_final_thermal,
1035            a_thermal.len(),
1036            a_tv.len()
1037        );
1038
1039        Ok(())
1040    }
1041
1042    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
1043    /// Average 100 experiments:
1044    //  Case 0: tc=161.691ms, obj=[4.824561313613876, 2576.8873693126284, 71.69242263342399, -1.048938713665848e-14]
1045    //  Case 1: tc=200.959ms, obj=[4.830121022357044, 2576.928260526994, 66.14588916597324, -7.651101974204267e-15]
1046    //  Case 2: tc=218.223ms, obj=[4.830230167437296, 2576.9529624501106, 66.12305109242398, -1.09470765785602e-14]
1047    //  Case 3: tc=93.588ms, obj=[361.45110144118144, 194870.1172166501, 48.05756488144189, 9.743247875171334e-16]
1048    //  Case 4: tc=164.505ms, obj=[4.824561266838617, 2576.887346690997, 71.69241993761281, 2.7478852526741092e-14]
1049    #[test]
1050    #[ignore = "slow"]
1051    fn test_copp2_socp_robust() -> Result<(), CoppError> {
1052        run_test_copp2_socp_repeated(100, true)?;
1053        Ok(())
1054    }
1055
1056    fn run_test_copp2_socp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
1057        let options_socp = ClarabelOptionsBuilder::new()
1058            .allow_almost_solved(true)
1059            .build()?;
1060
1061        let mut tc_sum_case0 = 0.0;
1062        let mut tc_sum_case1 = 0.0;
1063        let mut tc_sum_case2 = 0.0;
1064        let mut tc_sum_case3 = 0.0;
1065        let mut tc_sum_case4 = 0.0;
1066        let mut obj_sum_case0 = vec![0.0; 4];
1067        let mut obj_sum_case1 = vec![0.0; 4];
1068        let mut obj_sum_case2 = vec![0.0; 4];
1069        let mut obj_sum_case3 = vec![0.0; 4];
1070        let mut obj_sum_case4 = vec![0.0; 4];
1071
1072        for i_exp in 0..n_exp {
1073            let n: usize = 1000;
1074            let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
1075            let dim = robot.dim();
1076
1077            let mut rng = rand::rng();
1078            let (s, path, omega, phi) =
1079                lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1080            robot
1081                .with_s(&s.as_view())?
1082                .with_q_from_path_2nd(&path, 0, n)?;
1083            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
1084
1085            // Test different objectives in COPP2 optimization
1086            let objectives_test = [
1087                CoppObjective::Time(1.0),
1088                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1089                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1090                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1091            ];
1092            let a_feasible = vec![0.0; n];
1093
1094            // Case 0: Time only
1095            let mut copp2_problem = Copp2ProblemBuilder::new(
1096                &robot,
1097                (0, n - 1),
1098                (0.0, 0.0),
1099                &[CoppObjective::Time(1.0)],
1100            )
1101            .build()?;
1102            let start = Instant::now();
1103            let mut a_case0 = copp2_socp(&copp2_problem, &options_socp)?;
1104            let tc_copp2_case0 = start.elapsed().as_secs_f64() * 1E3;
1105            robot
1106                .constraints
1107                .project_to_feasible_topp2(&mut a_case0, &a_feasible, 0)?;
1108            let (_, obj_case0) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case0);
1109
1110            // Case 1: Time and ThermalEnergy
1111            let obj_case1 = [
1112                CoppObjective::Time(1.0),
1113                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1114            ];
1115            copp2_problem.objectives = &obj_case1;
1116            let start = Instant::now();
1117            let mut a_case1 = copp2_socp(&copp2_problem, &options_socp)?;
1118            let tc_copp2_case1 = start.elapsed().as_secs_f64() * 1E3;
1119            robot
1120                .constraints
1121                .project_to_feasible_topp2(&mut a_case1, &a_feasible, 0)?;
1122            let (_, obj_case1) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case1);
1123            if obj_case1[0] < obj_case0[0] - 1E-3 || obj_case1[1] - 1E-3 > obj_case0[1] {
1124                let (tf_case0, _) = s_to_t_topp2(s.as_slice(), &a_case0, 0.0)?;
1125                let (tf_case1, _) = s_to_t_topp2(s.as_slice(), &a_case1, 0.0)?;
1126                crate::verbosity_log!(
1127                    crate::diag::Verbosity::Summary,
1128                    "omega = {omega:?}\nphi = {phi:?}"
1129                );
1130                crate::verbosity_log!(
1131                    crate::diag::Verbosity::Summary,
1132                    "Case 0: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1133                    obj_case0[0],
1134                    obj_case0[1],
1135                    tf_case0
1136                );
1137                crate::verbosity_log!(
1138                    crate::diag::Verbosity::Summary,
1139                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1140                    obj_case1[0],
1141                    obj_case1[1],
1142                    tf_case1
1143                );
1144                crate::verbosity_log!(
1145                    crate::diag::Verbosity::Summary,
1146                    "Interesting... Cases 0 and 1"
1147                );
1148            }
1149
1150            // Case 2: Time and More ThermalEnergy
1151            let obj_case2 = [
1152                CoppObjective::Time(1.0),
1153                CoppObjective::ThermalEnergy(10.0, &vec![1.0; dim]),
1154            ];
1155            copp2_problem.objectives = &obj_case2;
1156            let start = Instant::now();
1157            let mut a_case2 = copp2_socp(&copp2_problem, &options_socp)?;
1158            let tc_copp2_case2 = start.elapsed().as_secs_f64() * 1E3;
1159            robot
1160                .constraints
1161                .project_to_feasible_topp2(&mut a_case2, &a_feasible, 0)?;
1162            let (_, obj_case2) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case2);
1163            if obj_case2[0] < obj_case1[0] - 1E-3 || obj_case2[1] - 1E-3 > obj_case1[1] {
1164                crate::verbosity_log!(
1165                    crate::diag::Verbosity::Summary,
1166                    "omega = {omega:?}\nphi = {phi:?}"
1167                );
1168                crate::verbosity_log!(
1169                    crate::diag::Verbosity::Summary,
1170                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1171                    obj_case1[0],
1172                    obj_case1[1]
1173                );
1174                crate::verbosity_log!(
1175                    crate::diag::Verbosity::Summary,
1176                    "Case 2: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1177                    obj_case2[0],
1178                    obj_case2[1]
1179                );
1180                crate::verbosity_log!(
1181                    crate::diag::Verbosity::Summary,
1182                    "Interesting... Cases 1 and 2"
1183                );
1184            }
1185
1186            // Case 3: Time and TotalVariationTorque
1187            let obj_case3 = [
1188                CoppObjective::Time(1.0),
1189                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1190            ];
1191            copp2_problem.objectives = &obj_case3;
1192            let start = Instant::now();
1193            let mut a_case3 = copp2_socp(&copp2_problem, &options_socp)?;
1194            let tc_copp2_case3 = start.elapsed().as_secs_f64() * 1E3;
1195            robot
1196                .constraints
1197                .project_to_feasible_topp2(&mut a_case3, &a_feasible, 0)?;
1198            let (_, obj_case3) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case3);
1199            if obj_case3[1] < obj_case1[1] - 1E-3 || obj_case3[2] - 1E-3 > obj_case1[2] {
1200                crate::verbosity_log!(
1201                    crate::diag::Verbosity::Summary,
1202                    "omega = {omega:?}\nphi = {phi:?}"
1203                );
1204                crate::verbosity_log!(
1205                    crate::diag::Verbosity::Summary,
1206                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1207                    obj_case1[0],
1208                    obj_case1[1],
1209                    obj_case1[2]
1210                );
1211                crate::verbosity_log!(
1212                    crate::diag::Verbosity::Summary,
1213                    "Case 3: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1214                    obj_case3[0],
1215                    obj_case3[1],
1216                    obj_case3[2]
1217                );
1218                crate::verbosity_log!(
1219                    crate::diag::Verbosity::Summary,
1220                    "Interesting... Cases 1 and 3"
1221                );
1222            }
1223
1224            // Case 4: Time and Linear
1225            let obj_case4 = [
1226                CoppObjective::Time(1.0),
1227                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1228            ];
1229            copp2_problem.objectives = &obj_case4;
1230            let start = Instant::now();
1231            let mut a_case4 = copp2_socp(&copp2_problem, &options_socp)?;
1232            let tc_copp2_case4 = start.elapsed().as_secs_f64() * 1E3;
1233            robot
1234                .constraints
1235                .project_to_feasible_topp2(&mut a_case4, &a_feasible, 0)?;
1236            let (_, obj_case4) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case4);
1237            if obj_case4[1] < obj_case1[1] - 1E-3 || obj_case4[3] - 1E-3 > obj_case1[3] {
1238                crate::verbosity_log!(
1239                    crate::diag::Verbosity::Summary,
1240                    "omega = {omega:?}\nphi = {phi:?}"
1241                );
1242                crate::verbosity_log!(
1243                    crate::diag::Verbosity::Summary,
1244                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1245                    obj_case1[0],
1246                    obj_case1[1],
1247                    obj_case1[3]
1248                );
1249                crate::verbosity_log!(
1250                    crate::diag::Verbosity::Summary,
1251                    "Case 4: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1252                    obj_case4[0],
1253                    obj_case4[1],
1254                    obj_case4[3]
1255                );
1256                crate::verbosity_log!(
1257                    crate::diag::Verbosity::Summary,
1258                    "Interesting... Cases 1 and 4"
1259                );
1260            }
1261            if obj_case4[2] < obj_case2[2] - 1E-3 || obj_case4[3] - 1E-3 > obj_case2[3] {
1262                crate::verbosity_log!(
1263                    crate::diag::Verbosity::Summary,
1264                    "omega = {omega:?}\nphi = {phi:?}"
1265                );
1266                crate::verbosity_log!(
1267                    crate::diag::Verbosity::Summary,
1268                    "Case 2: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1269                    obj_case2[0],
1270                    obj_case2[2],
1271                    obj_case2[3]
1272                );
1273                crate::verbosity_log!(
1274                    crate::diag::Verbosity::Summary,
1275                    "Case 4: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1276                    obj_case4[0],
1277                    obj_case4[2],
1278                    obj_case4[3]
1279                );
1280                crate::verbosity_log!(
1281                    crate::diag::Verbosity::Summary,
1282                    "Interesting... Cases 2 and 4"
1283                );
1284            }
1285
1286            if flag_print_step {
1287                crate::verbosity_log!(
1288                    crate::diag::Verbosity::Summary,
1289                    "Exp #{}:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1290                    i_exp + 1,
1291                    tc_copp2_case0,
1292                    obj_case0,
1293                    tc_copp2_case1,
1294                    obj_case1,
1295                    tc_copp2_case2,
1296                    obj_case2,
1297                    tc_copp2_case3,
1298                    obj_case3,
1299                    tc_copp2_case4,
1300                    obj_case4
1301                );
1302            }
1303
1304            tc_sum_case0 += tc_copp2_case0;
1305            tc_sum_case1 += tc_copp2_case1;
1306            tc_sum_case2 += tc_copp2_case2;
1307            tc_sum_case3 += tc_copp2_case3;
1308            tc_sum_case4 += tc_copp2_case4;
1309            for i in 0..obj_case0.len() {
1310                obj_sum_case0[i] += obj_case0[i];
1311                obj_sum_case1[i] += obj_case1[i];
1312                obj_sum_case2[i] += obj_case2[i];
1313                obj_sum_case3[i] += obj_case3[i];
1314                obj_sum_case4[i] += obj_case4[i];
1315            }
1316        }
1317
1318        for i in 0..4 {
1319            obj_sum_case0[i] /= n_exp as f64;
1320            obj_sum_case1[i] /= n_exp as f64;
1321            obj_sum_case2[i] /= n_exp as f64;
1322            obj_sum_case3[i] /= n_exp as f64;
1323            obj_sum_case4[i] /= n_exp as f64;
1324        }
1325
1326        crate::verbosity_log!(
1327            crate::diag::Verbosity::Summary,
1328            "Average {} experiments:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1329            n_exp,
1330            tc_sum_case0 / n_exp as f64,
1331            obj_sum_case0,
1332            tc_sum_case1 / n_exp as f64,
1333            obj_sum_case1,
1334            tc_sum_case2 / n_exp as f64,
1335            obj_sum_case2,
1336            tc_sum_case3 / n_exp as f64,
1337            obj_sum_case3,
1338            tc_sum_case4 / n_exp as f64,
1339            obj_sum_case4
1340        );
1341
1342        Ok(())
1343    }
1344
1345    fn run_test_copp2_socp_once(_options_socp: &ClarabelOptions) -> Result<(), CoppError> {
1346        run_test_copp2_socp_repeated(1, false)
1347    }
1348
1349    fn run_one_copp2_socp_only_time_case(
1350        options_ra: &ReachSet2Options,
1351        options_socp: &ClarabelOptions,
1352    ) -> Result<(f64, f64, f64, f64, f64, f64), CoppError> {
1353        let n: usize = 1000;
1354        let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
1355        let dim = robot.dim();
1356
1357        let mut rng = rand::rng();
1358        let (s, path, omega, phi) =
1359            lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1360        robot
1361            .with_s(&s.as_view())?
1362            .with_q_from_path_2nd(&path, 0, n)?;
1363        add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
1364
1365        let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
1366        let start = Instant::now();
1367        let a_ra = topp2_ra(&topp2_problem, options_ra)?;
1368        let tc_ra = start.elapsed().as_secs_f64() * 1E3;
1369        let (tf_ra, _) = s_to_t_topp2(s.as_slice(), &a_ra, 0.0)?;
1370
1371        let obj1 = [CoppObjective::Linear(
1372            1.0,
1373            &vec![-1.0; n],
1374            &vec![0.0; n - 1],
1375        )];
1376        let mut copp2_problem =
1377            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &obj1).build()?;
1378        let start = Instant::now();
1379        let a_lp = copp2_socp(&copp2_problem, options_socp)?;
1380        let tc_lp = start.elapsed().as_secs_f64() * 1E3;
1381        let (tf_lp, _) = s_to_t_topp2(s.as_slice(), &a_lp, 0.0)?;
1382
1383        copp2_problem.objectives = &[CoppObjective::Time(1.0)];
1384        let start = Instant::now();
1385        let a_qp = copp2_socp(&copp2_problem, options_socp)?;
1386        let tc_qp = start.elapsed().as_secs_f64() * 1E3;
1387        let (tf_qp, _) = s_to_t_topp2(s.as_slice(), &a_qp, 0.0)?;
1388
1389        if (tf_lp - tf_ra).abs() > 1e-3 || (tf_qp - tf_ra).abs() > 1e-3 {
1390            crate::verbosity_log!(
1391                crate::diag::Verbosity::Summary,
1392                "omega = {omega:?}\nphi = {phi:?}"
1393            );
1394            panic!("COPP2 time optimality failed!");
1395        }
1396
1397        Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp))
1398    }
1399
1400    fn run_test_copp2_socp_only_time_repeated(
1401        n_exp: usize,
1402        flag_print_step: bool,
1403    ) -> Result<(), CoppError> {
1404        let mut tc_sum_ra = 0.0;
1405        let mut tc_sum_lp = 0.0;
1406        let mut tc_sum_qp = 0.0;
1407        let mut tf_sum_ra = 0.0;
1408        let mut tf_sum_lp = 0.0;
1409        let mut tf_sum_qp = 0.0;
1410
1411        let options_ra = ReachSet2OptionsBuilder::new()
1412            .lp_feas_tol(1E-9)
1413            .a_cmp_abs_tol(1E-9)
1414            .a_cmp_rel_tol(1E-9)
1415            .build()?;
1416        let options_socp = ClarabelOptionsBuilder::new()
1417            .allow_almost_solved(true)
1418            .build()?;
1419
1420        for i_exp in 0..n_exp {
1421            let (tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp) =
1422                run_one_copp2_socp_only_time_case(&options_ra, &options_socp)?;
1423
1424            if flag_print_step {
1425                crate::verbosity_log!(
1426                    crate::diag::Verbosity::Summary,
1427                    "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1428                    i_exp + 1,
1429                    tc_ra,
1430                    tc_lp,
1431                    tc_qp,
1432                    tf_ra,
1433                    tf_lp,
1434                    tf_qp,
1435                );
1436            }
1437
1438            tc_sum_ra += tc_ra;
1439            tc_sum_lp += tc_lp;
1440            tc_sum_qp += tc_qp;
1441            tf_sum_ra += tf_ra;
1442            tf_sum_lp += tf_lp;
1443            tf_sum_qp += tf_qp;
1444        }
1445
1446        crate::verbosity_log!(
1447            crate::diag::Verbosity::Summary,
1448            "Average {} experiments: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1449            n_exp,
1450            tc_sum_ra / n_exp as f64,
1451            tc_sum_lp / n_exp as f64,
1452            tc_sum_qp / n_exp as f64,
1453            tf_sum_ra / n_exp as f64,
1454            tf_sum_lp / n_exp as f64,
1455            tf_sum_qp / n_exp as f64
1456        );
1457
1458        Ok(())
1459    }
1460}